package com.nulldev.util.io;

import static com.nulldev.util.manage.Macros.__assert;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;

import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.Charsets.CharsetUtil;

public class NIOUtils {

	private static final byte[] CHUNK_EOF = "\r\n".getBytes();

	public static int writeAll(final WritableByteChannel out, final ByteBuffer data) throws IOException {
		Variables.requireNonNull(out, "out");
		Variables.requireNonNull(data, "data");
		int sent = 0;
		while (data.hasRemaining()) {
			sent += out.write(data);
		}
		return sent;
	}

	public static void writeAll(final WritableByteChannel wbc, final byte[] data) throws IOException {
		final ByteBuffer _data = ByteBuffer.wrap(data);
		writeAll(wbc, _data);
	}

	public static void writeAllChunked(final WritableByteChannel wbc, final byte[] byteArray) throws IOException {
		final ByteBuffer _len = ByteBuffer.wrap((MathUtil.toHex(byteArray.length) + "\r\n").getBytes(CharsetUtil.US_ASCII));
		writeAll(wbc, _len);
		writeAll(wbc, byteArray);
		writeAll(wbc, CHUNK_EOF);
	}

	public static ByteBuffer clone(final ByteBuffer original) {
		final ByteBuffer clone = ByteBuffer.allocate(original.capacity());
		original.rewind();
		clone.put(original);
		original.rewind();
		clone.flip();
		return clone;
	}

	public static void printMetrics(final ByteBuffer buffer) {
		Variables.requireNonNull(buffer, "buffer");
		JVM.println(String.format("position = %4d, limit = %4d, capacity = %4d, remaining = %4d", buffer.position(), buffer.limit(), buffer.capacity(),
				buffer.remaining()));
	}

	public static ByteChannel fromIO(final InputStream is, final OutputStream os) {
		Variables.requireNonNull(is, "is");
		Variables.requireNonNull(os, "os");
		return new ByteChannelIO(is, os);
	}

	private static class ByteChannelIO implements ByteChannel {

		private final ReadableByteChannel read;
		private final WritableByteChannel write;

		public ByteChannelIO(final InputStream is, final OutputStream os) {
			this.read = IOUtils.copyMakeChannel(Variables.requireNonNullObject(is, "is"));
			this.write = IOUtils.copyMakeChannel(Variables.requireNonNullObject(os, "os"));
		}

		@Override
		public int read(ByteBuffer dst) throws IOException {
			return this.read.read(dst);
		}

		@Override
		public boolean isOpen() {
			return this.read.isOpen() && this.write.isOpen();
		}

		@Override
		public void close() throws IOException {
			this.read.close();
			this.write.close();
		}

		@Override
		public int write(ByteBuffer src) throws IOException {
			return this.write.write(src);
		}

	}

	public static ByteChannel combine(final ReadableByteChannel readChannel, final WritableByteChannel writeChannel) {
		Variables.requireNonNull(readChannel, "readChannel");
		Variables.requireNonNull(writeChannel, "writeChannel");
		__assert(!readChannel.isOpen(), "!readChannel.isOpen");
		__assert(!writeChannel.isOpen(), "!writeChannel.isOpen");
		return new ByteChannel() {

			@Override
			public int write(final ByteBuffer src) throws IOException {
				return writeChannel.write(src);
			}

			@Override
			public boolean isOpen() {
				return readChannel.isOpen() && writeChannel.isOpen();
			}

			@Override
			public void close() throws IOException {
				readChannel.close();
				writeChannel.close();
			}

			@Override
			public int read(final ByteBuffer dst) throws IOException {
				return readChannel.read(dst);
			}
		};
	}

	public static ReadableByteChannel makeReadableByteChannel(final InputStream is) {
		return IOUtils.copyMakeChannel(is);
	}

	public static WritableByteChannel makeWritableByteChannel(final OutputStream out) {
		return IOUtils.copyMakeChannel(out);
	}

	public static OutputStream makeOutputStream(final WritableByteChannel out) {
		Variables.requireNonNull(out, "out");
		return new BufferedOutputStream(new OutputStream() {

			@Override
			public void write(int by) throws IOException {
				final ByteBuffer b = ByteBuffer.wrap(new byte[]
					{ (byte) by });
				while (b.hasRemaining()) {
					out.write(b);
				}
			}

			@Override
			public void write(final byte[] buf) throws IOException {
				final ByteBuffer b = ByteBuffer.wrap(buf);
				while (b.hasRemaining()) {
					out.write(b);
				}
			}

			@Override
			public void write(byte[] buf, int off, int len) throws IOException {
				final ByteBuffer b = ByteBuffer.wrap(buf, off, len);
				while (b.hasRemaining()) {
					out.write(b);
				}
			}

			@Override
			public void flush() throws IOException {
			}

			@Override
			public void close() throws IOException {
				out.close();
			}
		}, IOUtils.MEMORY_ALLOC_BUFFER);
	}
}
